<?php
namespace Plu\Library\Logger;

use Phalcon\Logger\Adapter,
    Phalcon\Logger\Exception,
    Phalcon\Logger\Formatter\Line as LineFormatter,
    Phalcon\Logger,
    Phalcon\Logger\Adapter\File as LoggerEngine;

use Thrift\Transport\TCurlClient,
    Thrift\Protocol\TCompactProtocol;

use Plu\Library\Logger\Metric\Thrift\TLogEntity,
    Plu\Library\Logger\Metric\Thrift\TLogPackage;

use Httpful\Request;

/**
 * Logger metric系统
 * 访问日志：http://prod-logviewer.longzhu.cn/server/logviewer.html
 * $logger = new MetricLogger('passport', [
 *     'server' => '115.231.106.244',
 *     'port'   => 9093,
 *     'uri'    => '/server',
 *     'scheme' => 'http',
 *     'appid'  => 9001
 * ]);
 */
class Metric extends Adapter {
    protected $name = null;

    protected $options = null;

    protected $logs = [];

    protected $isTransaction = false;

    protected $client = null;

    private $localIPAdrress = null;

    private $yac = null;

    private $defaultLogger = null;

    const LOG_LEVEL_DEBUG = 1;

    const LOG_LEVEL_INFO = 2;

    const LOG_LEVEL_WARNING = 3;

    const LOG_LEVEL_ERROR = 4;

    const PATTERN_IPV4 = '/((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4}/';
    
    const DEFAULT_LOG_CONFIG = [
        self::LOG_LEVEL_DEBUG   => 1,
        self::LOG_LEVEL_INFO    => 1,
        self::LOG_LEVEL_WARNING => 1,
        self::LOG_LEVEL_ERROR   => 1
    ];

    /**
     * @param string $name
     */
    public function __construct($name, $options = []) {
        $this->options= $options;
        $this->name = $name;

        if (class_exists("\\Yac") && isset($options['server']) && isset($options['uri']) && isset($options['appid'])) {
            $this->yac = new \Yac('~metric~appid_'.$options['appid'].'_');

            if ($this->yac->get('serverdown')) {
                $this->defaultLogger = $this->getDefaultLogger();
            } else {
                //get log on/off
                $this->logConfig = $this->getLogConfig($options['appid']);
            }
        } else {
            $this->defaultLogger = $this->getDefaultLogger();
        }

        register_shutdown_function([$this, 'commit']);
        register_shutdown_function([$this, 'close']);
    }

    //10分钟缓存
    private function getLogConfig($appId) {
        $config = $this->yac->get('config');

        if (!empty($config)) {
            return $config;
        }

        $uri = sprintf("%s://%s:%s/%s/%s?%s", 
            isset($this->options['scheme']) ? $this->options['scheme'] : 'http',
            $this->options['server'],
            isset($this->options['port'])   ? $this->options['port'] : 80,
            $this->options['uri'], "GetLogOnOff.ashx", http_build_query([
                'appId' => $appId
            ]));
            
        try {
            $response = Request::get($uri)->send();

            if ($response->hasErrors() || empty($response->raw_body)) {
                $config = self::DEFAULT_LOG_CONFIG;
            } else {
                $config = array_combine(array_keys(self::DEFAULT_LOG_CONFIG), explode(",", $response->raw_body));
            }
        } catch (\Exception $e) {
            $config = self::DEFAULT_LOG_CONFIG;
        }

        $this->yac->set("config", $config, 10 * 60);

        return $config;
    }

    public function getDefaultLogger() {
        $logfile_dir = join(DIRECTORY_SEPARATOR, array(APP_PATH, "logs"));
        if (!is_dir($logfile_dir)) {
            @mkdir($logfile_dir, 0777, true);
            chmod($logfile_dir, 0777);
        }

        $logger = new LoggerEngine(join(DIRECTORY_SEPARATOR, array($logfile_dir, date("Y-m-d", time()).".log")));
        $logfile = $logger->getPath();
        chmod($logfile, 0777);

        return $logger;
    }

    public function getFormatter() {
        if (!$this->_formatter) {
            $this->_formatter = new LineFormatter();
        }
        return $this->_formatter;
    }

    private function initClient() {
        $uri = sprintf("%s/%s", $this->options['uri'], "Reciver.ashx");
        $port = 80;
        $scheme = 'http';

        if (isset($this->options['port'])) {
            $port = $this->options['port'];
        }

        if (isset($this->options['scheme'])) {
            $scheme = $this->options['scheme'];
        }

        $transport = new TCurlClient($this->options['server'], $port, $uri, $scheme);
        $transport->setTimeoutSecs(0.5);

        $protocol = new TCompactProtocol($transport);

        $client = new Metric\Thrift\LogTransferServiceClient($protocol);

        $transport->open();

        $this->client = $client;
    }

    /**
     * 写入日志
     */
    public function logInternal($message, $type, $time, $context = []) {
        switch ($type) {
            case Logger::SPECIAL:
            case Logger::DEBUG:
            case Logger::CUSTOM:
                $loggerLevel = 1;
                break;
            case Logger::INFO:
            case Logger::NOTICE:
                $loggerLevel = 2;
                break;
            case Logger::WARNING:
            case Logger::ALERT:
                $loggerLevel = 3;
                break;
            case Logger::ERROR:
            case Logger::CRITICAL:
            case Logger::EMERGENCE:
            case Logger::EMERGENCY:
                $loggerLevel = 4;
                break;
            default:
                $loggerLevel = 1;
                break;
        }

        if (isset($this->defaultLogger)) {
            $this->defaultLogger->log($type, $message . " " . json_encode($context));
        } else {
            //jump
            if ($this->logConfig[$loggerLevel] == 0) {
                return;
            }

            $msg = $message;

            if (!empty($context) && is_array($context)) {
                foreach ($context as $k => $v) {
                    if (!is_string($v)) {
                        $v = json_encode($v);
                    }
                    $msg .= "\r\n $k: $v";
                }
            }

            $this->logs[] = new TLogEntity([
                'Title'     => $message,
                'Message'   => $msg,
                'Level'     => $loggerLevel,
                'Time'      => microtime(true) * 10000000,
                'Source'    => $this->name,
                'Thread'    => getmypid(),
            ]);

            if (!$this->isTransaction) {
                $this->send();
            }
        }
    }

    /**
     * always return true
     */
    public function close() {
        return true;
    }

    public function begin() {
        $this->commit();
        $this->isTransaction = true;
    }

    public function commit() {
        if (!$this->isTransaction || empty($this->logs)) {
            $this->isTransaction = false;
            return;
        }

        $this->send();
        $this->isTransaction = false;
    }

    private function getServerIP() {
        if (!is_null($this->localIPAdrress)) {
            return $this->localIPAdrress;
        }

        $defaultAddress = "127.0.0.1";
        
        $localPort = 0;

        if (function_exists('socket_create')) {
            //SOCK_DGRAM:
            //  Supports datagrams (connectionless, unreliable messages of a fixed maximum length). The UDP protocol is based on this socket type.
            $socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
            socket_connect($socket, $this->options['server'], $this->options['port']);
            socket_getsockname($socket, $this->localIPAdrress, $localPort);
            socket_close($socket);
        } else {
            $hostname = gethostname();
            if (preg_match(self::PATTERN_IPV4, $hostname)) {
                $this->localIPAdrress = $hostname;
            } else {
                $addr = gethostbyname($hostname);
                if (preg_match(self::PATTERN_IPV4, $addr)) {
                    $this->localIPAdrress = $addr;
                }
            }
        }

        if (empty($this->localIPAdrress)) {
            $this->localIPAdrress = $defaultAddress;
        }

        return $this->localIPAdrress;
    }

    public function send() {
        //file log
        if (isset($this->defaultLogger)) {
            return;
        }

        //do nothing when, the logview server is down.
        if ($this->yac->get("serverdown") !== false) {
            restore_error_handler();
            restore_exception_handler();
            return;
        }
        
        if ($this->client == null) {
            $this->initClient();
        }

        if (empty($this->logs)) {
            return;
        }
        
        $package = new TLogPackage([
            'IP' => ip2long($this->getServerIP()),
            'AppId' => (int)$this->options['appid'],
            'LogItems' => $this->logs
        ]);

        try {
            $this->client->send_log($package);
        } catch (\Exception $e) {
            restore_error_handler();
            restore_exception_handler();
            $this->yac->set("serverdown", 'DOWN', 60*30); // 30 min wait for logview server recover;
        }

        $this->logs = [];
    }
}
